NexusPi Git Node
Commit 079b0ac4b652efd398264b104230ebfed24822a5
Parents : 37061cf
Author : James L <jrl290@gmail.com>
Date : 2026-05-08T20:33:22-04:00
v1.0.32 — LED off, display cleanup, TCP keepalive fix
FIREWALL_MODE: LED (GPIO 35) is now fully off at all times.
- pin_led_rx/tx driven LOW at init
- headless_led_solid() skipped in FIREWALL_MODE
- led_rx_on/off and led_tx_on/off are no-ops in FIREWALL_MODE on V4
Display: removed mDNS hostname row; title bar shows device name only.
Body simplified to single layout: IP + port, consistent separator.
TCP: fix half-open backbone connections
- Check keepalive write() return; 0 bytes -> immediate _cleanup_client()
- OS-level TCP keepalive: SO_KEEPALIVE + TCP_KEEPIDLE=10s + KEEPINTVL=5s
+ KEEPCNT=3 — kernel detects dead socket in ~25s independently
Changes
4 files changed, 55 insertions(+), 48 deletions(-)
Diff
diff --git a/Display.h b/Display.h
index aacb758..1f9f8af 100755
--- a/Display.h
+++ b/Display.h
@@ -943,47 +943,21 @@ void draw_disp_area() {
// 1px separator after SF line
disp_area.drawLine(0, 34, disp_area.width()-1, 34, SSD1306_WHITE);
- // When mDNS is enabled show three tighter rows: hostname, IP, port.
- // When disabled keep the original two-row layout (IP, port, separator).
- if (firewall_state.mdns_enabled) {
- // Row 1: mDNS hostname (resolved at draw time)
- uint8_t mac[6];
- WiFi.macAddress(mac);
- char suffix[5];
- snprintf(suffix, sizeof(suffix), "%02x%02x", mac[4], mac[5]);
- char host[33];
- mdns_service::resolve_hostname(firewall_state.mdns_hostname, suffix,
- host, sizeof(host));
- disp_area.setCursor(2, 42);
- disp_area.printf("%s.local", host);
-
- // Row 2: WiFi IP
- disp_area.setCursor(2, 52);
- if (firewall_state.wifi_connected) {
- disp_area.print(wr_device_ip);
- } else {
- disp_area.print("No WiFi");
- }
-
- // Row 3: Local TCP server port (shown only when enabled)
- disp_area.setCursor(2, 62);
- if (firewall_state.ap_tcp_enabled) {
- disp_area.printf("Port:%u", firewall_state.ap_tcp_port);
- }
+ // IP address
+ disp_area.setCursor(2, 44);
+ if (firewall_state.wifi_connected) {
+ disp_area.print(wr_device_ip);
} else {
- // Original two-row layout — IP, port, separator.
- disp_area.setCursor(2, 44);
- if (firewall_state.wifi_connected) {
- disp_area.print(wr_device_ip);
- } else {
- disp_area.print("No WiFi");
- }
- disp_area.setCursor(2, 55);
- if (firewall_state.ap_tcp_enabled) {
- disp_area.printf("Port:%u", firewall_state.ap_tcp_port);
- }
- disp_area.drawLine(0, 60, disp_area.width()-1, 60, SSD1306_WHITE);
+ disp_area.print("No WiFi");
}
+
+ // Local TCP server port (shown only when enabled)
+ disp_area.setCursor(2, 55);
+ if (firewall_state.ap_tcp_enabled) {
+ disp_area.printf("Port:%u", firewall_state.ap_tcp_port);
+ }
+
+ disp_area.drawLine(0, 60, disp_area.width()-1, 60, SSD1306_WHITE);
#else
if (radio_online && display_diagnostics) {
#ifdef HAS_RNS
diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino
index 126b301..03d7dfd 100755
--- a/RNode_Firmware.ino
+++ b/RNode_Firmware.ino
@@ -383,6 +383,11 @@ void setup() {
#if HAS_NP == false
pinMode(pin_led_rx, OUTPUT);
pinMode(pin_led_tx, OUTPUT);
+ #ifdef FIREWALL_MODE
+ // Keep the LED off in Firewall Mode — the OLED is the status indicator.
+ digitalWrite(pin_led_rx, LOW);
+ digitalWrite(pin_led_tx, LOW);
+ #endif
#endif
#if HAS_TCXO == true
@@ -496,8 +501,9 @@ void setup() {
}
#endif
- // LED solid on at boot for V3/V4 boards (with or without display)
- #if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
+ // LED solid on at boot for V3/V4 boards (with or without display).
+ // In FIREWALL_MODE the OLED is the status indicator — keep the LED off.
+ #if (BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3) && !defined(FIREWALL_MODE)
headless_led_solid();
#endif
@@ -2516,11 +2522,11 @@ void loop() {
if (wifi_is_connected()) {
if (tcp_interface_ptr && !tcp_interface_ptr->isStarted()) {
tcp_interface_ptr->start();
- Serial.println("[Boundary] WiFi connected, TCP backbone started");
+ Serial.println("[Firewall] WiFi connected, TCP backbone started");
}
if (local_tcp_interface_ptr && !local_tcp_interface_ptr->isStarted()) {
local_tcp_interface_ptr->start();
- Serial.println("[Boundary] WiFi connected, local TCP server started");
+ Serial.println("[Firewall] WiFi connected, local TCP server started");
}
}
if (tcp_interface_ptr) {
@@ -2614,8 +2620,9 @@ void loop() {
if (disp_ready && !display_updating) update_display();
#endif
- // LED solid when operational on V3/V4 boards (yield to fast blink during white screen)
- #if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
+ // LED solid when operational on V3/V4 boards (yield to fast blink during white screen).
+ // In FIREWALL_MODE the OLED is the status indicator — keep the LED off.
+ #if (BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3) && !defined(FIREWALL_MODE)
if (radio_online && !display_lock_white) {
headless_led_solid();
}
diff --git a/TcpInterface.h b/TcpInterface.h
index afb585f..b3ab44c 100755
--- a/TcpInterface.h
+++ b/TcpInterface.h
@@ -18,6 +18,7 @@
#include <WiFi.h>
#include <lwip/sockets.h> // SO_LINGER — force RST to free lwIP PCBs immediately
+#include <netinet/tcp.h> // TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT
#include <Interface.h>
#include <Transport.h>
#include <Bytes.h>
@@ -182,7 +183,9 @@ public:
}
}
- // Send keepalive (empty HDLC frames) to prevent read timeout on both sides
+ // Send keepalive (empty HDLC frames) to detect dead connections quickly.
+ // Checking the write result catches half-open connections (remote host
+ // disappeared without sending FIN) without waiting for the read timeout.
if (_num_clients > 0) {
uint32_t now = millis();
if (now - _last_keepalive >= TCP_IF_KEEPALIVE_INTERVAL) {
@@ -190,11 +193,13 @@ public:
uint8_t ka[] = { HDLC_FLAG, HDLC_FLAG };
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
if (_clients[i].active && _clients[i].client.connected()) {
- _clients[i].client.write(ka, 2);
+ size_t written = _clients[i].client.write(ka, 2);
+ if (written == 0) {
+ _cleanup_client(i, "keepalive write failed");
+ }
}
}
}
-
}
// Process incoming data from all active clients
@@ -440,6 +445,20 @@ private:
if (connected) {
client.setNoDelay(true);
client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000);
+ // Enable OS-level TCP keepalives so the kernel probes the connection
+ // after 10s idle, retrying every 5s up to 3 times (15s detection window).
+ // This catches half-open connections independently of our app-layer keepalive.
+ int fd = client.fd();
+ if (fd >= 0) {
+ int optval = 1;
+ setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
+ int idle = 10; // seconds idle before first probe
+ int intvl = 5; // seconds between retries
+ int cnt = 3; // retries before declaring dead
+ setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
+ setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
+ setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
+ }
_clients[0].client = client;
_clients[0].active = true;
_clients[0].in_frame = false;
diff --git a/Utilities.h b/Utilities.h
index 7b42eaf..f6dfb7c 100755
--- a/Utilities.h
+++ b/Utilities.h
@@ -318,10 +318,17 @@ extern RNS::Reticulum reticulum;
void led_id_on() { }
void led_id_off() { }
#elif BOARD_MODEL == BOARD_HELTEC32_V4
+ #ifdef FIREWALL_MODE
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ #else
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ #endif
void led_id_on() { }
void led_id_off() { }
#elif BOARD_MODEL == BOARD_LORA32_V2_1
Served by rngit 1.5.2 - Generated in 0.12s